﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Mail;
using System.Text;
using System.Threading;
using System.Timers;
using BLL.Workflows;
using DO.Workflows;
using Timer=System.Timers.Timer;

namespace BLL.Workers
{
    public class WorkflowService
    {
        #region fields
        private bool _IsActive;

        private Dictionary<Guid, WorkflowDesign> _AllWorkflowDesigns;

        // settings 
        private Timer _WorkflowRetrievalTimer;
        private Timer _WorkflowDispatcherTimer;
        private int _DispatcherTimeInterval = 1000 * 30;  // 30 sec
        public int DispatcherInterval
        {
            get
            {
                return _DispatcherTimeInterval;
            }
            set
            {
                _DispatcherTimeInterval = value;
            }
        }
        private int _RetrievalTimeInterval = 1000 * 60 * 5; // 5 min
        public int RetrievalInterval
        {
            get
            {
                return _RetrievalTimeInterval;
            }
            set
            {
                _RetrievalTimeInterval = value;
            }
        }
        private EventLog _EventLog;
        private readonly string _LogName = "WORKFLOW ENGINE";
        private readonly string _LogSource = "WorkflowService";
        #endregion

        #region public
        /// <summary></summary>
        public WorkflowService()
        {
            if (ConfigurationManager.AppSettings["WorkflowRetrievalInterval"] != null)
                this._RetrievalTimeInterval =
                    int.Parse(ConfigurationManager.AppSettings.Get("WorkflowRetrievalInterval"));
            if (ConfigurationManager.AppSettings["WorkflowDispatcherInterval"] != null)
                this._DispatcherTimeInterval =
                    int.Parse(ConfigurationManager.AppSettings.Get("WorkflowDispatcherInterval"));
            if (ConfigurationManager.AppSettings["EventLogName"] != null)
                this._LogName = ConfigurationManager.AppSettings.Get("EventLogName");
            if (ConfigurationManager.AppSettings["EventLogSource"] != null)
                this._LogSource = ConfigurationManager.AppSettings.Get("EventLogSource");
            this.RegisterEventLog();
            this._AllWorkflowDesigns = new Dictionary<Guid, WorkflowDesign>();
        }

        private void RegisterEventLog()
        {
            if (!EventLog.SourceExists(this._LogSource))
            {
                EventLog.CreateEventSource(this._LogSource, this._LogName);
            }
            this._EventLog = new EventLog(this._LogName, ".", this._LogSource);
            this._EventLog.MaximumKilobytes = 4096;
            this._EventLog.ModifyOverflowPolicy(OverflowAction.OverwriteAsNeeded, 7);
        }

        /// <summary></summary>
        public void Start()
        {
            this.StartRetrieval();
            this.StartDispatcher();
            this._IsActive = true;
        }

        /// <summary></summary>
        public void Stop()
        {
            this.StopRetrieval();
            this.StopDispatcher();
            this._IsActive = false;
        }

        /// <summary></summary>
        public bool IsActive
        {
            get
            {
                return this._IsActive;
            }
        }
        #endregion

        #region timer
        private void StartRetrieval()
        {
            if (this._WorkflowRetrievalTimer != null)
            {
                this._WorkflowRetrievalTimer.Dispose();
                this._WorkflowRetrievalTimer = null;
            }
            GC.Collect();
            GC.WaitForFullGCComplete();
            this._WorkflowRetrievalTimer = new Timer(this._RetrievalTimeInterval);
            this._WorkflowRetrievalTimer.Enabled = true;
            this._WorkflowRetrievalTimer.Elapsed += new ElapsedEventHandler(SynchronizeWorkflowsFromStore);
            this._WorkflowRetrievalTimer.Start();
        }

        private void StartDispatcher()
        {
            if (this._WorkflowDispatcherTimer != null)
            {
                this._WorkflowDispatcherTimer.Dispose();
                this._WorkflowDispatcherTimer = null;
            }
            GC.Collect();
            GC.WaitForFullGCComplete();
            this._WorkflowDispatcherTimer = new Timer(this._DispatcherTimeInterval);
            this._WorkflowDispatcherTimer.Enabled = true;
            this._WorkflowDispatcherTimer.Elapsed += new ElapsedEventHandler(DispatchWorkflows);
            this._WorkflowDispatcherTimer.Start();
        }

        private void StopRetrieval()
        {
            this._WorkflowRetrievalTimer.Stop();
        }

        private void StopDispatcher()
        {
            this._WorkflowDispatcherTimer.Stop();
        }

        #endregion

        #region retrieval
        void SynchronizeWorkflowsFromStore(object sender, ElapsedEventArgs e)
        {
            this.StopRetrieval();
            //this.StopDispatcher();
            lock (this._AllWorkflowDesigns)
            {
                List<WorkflowDesign> wfDesigns = WorkflowManager.GetAllWorkflows();
                if (this._AllWorkflowDesigns == null)
                    this._AllWorkflowDesigns = new Dictionary<Guid, WorkflowDesign>();
                if (wfDesigns != null && wfDesigns.Count > 0)
                {
                    foreach (WorkflowDesign wfDesign in wfDesigns)
                    {
                        try
                        {
                            WorkflowTrigger trigger = WorkflowManager.GetWorkflowTriggerByWorkflow(wfDesign);
                            if (trigger != null && trigger.CanRun())
                            {
                                // WorkflowInstance wfInstance = this._Engine.CreateNewWorkflowInstance(wfDesign);
                                if (!this._AllWorkflowDesigns.ContainsKey(wfDesign.WorkflowID))
                                    this._AllWorkflowDesigns.Add(wfDesign.WorkflowID, wfDesign);
                            }
                        }
                        catch(Exception ex)
                        {
                            this.LogErrorMessage(ex.Message);
                        }
                    }
                }

            }
            this.StartRetrieval();
            //this.StartDispatcher();
        }
        #endregion

        #region dispatcher
        void DispatchWorkflows(object sender, ElapsedEventArgs e)
        {
            this.StopDispatcher();
            lock (this._AllWorkflowDesigns)
            {
                try
                {
                    if (this._AllWorkflowDesigns != null && this._AllWorkflowDesigns.Count > 0)
                    {
                        foreach (WorkflowDesign wfDesign in this._AllWorkflowDesigns.Values)
                        {
                            try
                            {
                                WorkflowTrigger trigger = WorkflowManager.GetWorkflowTriggerByWorkflow(wfDesign);
                                if(trigger!=null && trigger.CanRun())
                                {
                                    WorkflowTriggerInstructionBase triggerInstruction =(WorkflowTriggerInstructionBase) trigger.Instruction;
                                    ResultBase[] triggerOutputs = trigger.ProcessTrigger(ref triggerInstruction);
                                    if(triggerOutputs!=null)
                                    {
                                        trigger.Instruction = triggerInstruction;
                                        
                                        foreach(ResultBase triggerOutput in triggerOutputs)
                                        {
                                            this.OnWorkflowTriggerFired(wfDesign, trigger, triggerOutput);
                                        }

                                        DateTime now = DateTime.Now;
                                        ((WorkflowTriggerInstructionBase)trigger.Instruction).LastRunTime = now;
                                        TimeSpan span = ((WorkflowTriggerInstructionBase) trigger.Instruction).Span;
                                        ((WorkflowTriggerInstructionBase)trigger.Instruction).NextRunTime = now.Add(span);
                                        WorkflowManager.ResetWorkflowTrigger(trigger);
                                    }
                                }
                            }
                            catch (Exception wfServiceException)
                            {
                                this.LogErrorMessage("Error occurred in while processing workflow trigger: " + wfServiceException.ToString());
                                this.LogErrorMessage(wfServiceException.StackTrace);
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    this.LogErrorMessage("Error while dispatching workflow: " + ex.ToString());
                    this.LogErrorMessage(ex.StackTrace);
                }
            }
            this.StartDispatcher();
        }
        #endregion

        #region create run and jobs
        private void OnWorkflowTriggerFired(WorkflowDesign wfDesign, WorkflowTrigger trigger, ResultBase triggerOutput)
        {
            WorkflowManager.SaveJobOutput(triggerOutput);
            Run newRun = CreateRunFromWorkflow(wfDesign);

            Dictionary<Guid, Job> componentJobs=new Dictionary<Guid, Job>();
            Dictionary<Job,InstructionBase> jobInstructions=new Dictionary<Job, InstructionBase>();
            Dictionary<Job,ResultBase> jobOutputs=new Dictionary<Job, ResultBase>();
            List<Guid> disabledComponentIDs=new List<Guid>();
            foreach(WorkflowComponentBase component in wfDesign.Components.Values)
            {
                if(component.ComponentID==trigger.ComponentID)
                    continue;
                if(!component.Enabled)
                {
                    disabledComponentIDs.Add(component.ComponentID);
                    continue;
                }
                Guid jobID = Guid.NewGuid();
                InstructionBase jobInstruction = component.Instruction;
                jobInstruction.JobTicketID = jobID;
                WorkflowManager.SaveJobInstruction(jobInstruction);
                ResultBase jobOutput = component.Output;
                jobOutput.JobID = jobID;
                WorkflowManager.SaveJobOutput(jobOutput);
                Job job =
                    new Job
                        {
                            JobID = jobID,
                            RunID = newRun.RunID,
                            ComponentID=component.ComponentID,
                            Dependencies=new List<Guid>(),
                            HasError = false,
                            IsFirstJob = false,
                            IsLastJob = false,
                            Cancelled = false,
                            Priority = ThreadPriority.Normal,
                            ReceiveTime = null,
                            SubmissionTime = DateTime.Now,
                            StartTime = null,
                            FinishTime = null,
                        };
                componentJobs.Add(component.ComponentID, job);
                jobInstructions.Add(job,jobInstruction);
                jobOutputs.Add(job,jobOutput);
            }
            List<Guid> allComponentIDs=new List<Guid>();
            allComponentIDs.AddRange(componentJobs.Keys);
            foreach (Guid componentID in allComponentIDs)
            {
                List<Guid> jobDependencies = new List<Guid>();
                if (wfDesign.SuccessConnectors != null && wfDesign.SuccessConnectors.Count > 0)
                {
                    foreach (Guid fromComponentID in wfDesign.SuccessConnectors.Keys)
                    {
                        if (fromComponentID == trigger.ComponentID)
                            continue;
                        if (wfDesign.SuccessConnectors[fromComponentID].Contains(componentID))
                        {
                            if (!jobDependencies.Contains(componentJobs[fromComponentID].JobID))
                                jobDependencies.Add(componentJobs[fromComponentID].JobID);
                        }
                    }
                }
                if(wfDesign.FailureConnectors!=null && wfDesign.FailureConnectors.Count>0)
                {
                    foreach (Guid fromComponentID in wfDesign.SuccessConnectors.Keys)
                    {
                        if (fromComponentID == trigger.ComponentID)
                            continue;
                        if (wfDesign.FailureConnectors[fromComponentID].Contains(componentID))
                        {
                            if (!jobDependencies.Contains(componentJobs[fromComponentID].JobID))
                                jobDependencies.Add(componentJobs[fromComponentID].JobID);
                        }
                    }
                }
                componentJobs[componentID].Dependencies = jobDependencies;
                if(jobDependencies.Count==0)
                {
                    if(wfDesign.DataPipeConverters.ContainsKey(trigger.ComponentID) && 
                        wfDesign.DataPipeConverters[trigger.ComponentID].ContainsKey(componentID))
                    {
                        List<CodeFieldMapper> fieldMappers =
                            wfDesign.DataPipeConverters[trigger.ComponentID][componentID];
                        Job job = componentJobs[componentID];
                        InstructionBase jobInstruction = jobInstructions[job];
                        WorkflowManager.UpdateJobInstruction(ref jobInstruction, triggerOutput, fieldMappers.ToArray());
                        WorkflowManager.SaveJobInstruction(jobInstruction);
                    }
                    componentJobs[componentID].IsFirstJob = true;
                }
                if(wfDesign.SuccessConnectors==null || !wfDesign.SuccessConnectors.ContainsKey(componentID))
                {
                    if(wfDesign.FailureConnectors==null || !wfDesign.FailureConnectors.ContainsKey(componentID))
                        componentJobs[componentID].IsLastJob = true;
                }
            }
            foreach(Job job in componentJobs.Values)
            {
                WorkflowManager.AddJob(job);
            }
        }

        /// <summary></summary>
        private Run CreateRunFromWorkflow(WorkflowDesign wfDesign)
        {
            Run run = new Run
                          {
                              FinishTime = null,
                              RunID = Guid.NewGuid(),
                              SubmitTime = DateTime.Now,
                              ReceiveTime=null,
                              StartTime = null,
                              Variables = wfDesign.Vars,
                              WorkflowID = wfDesign.WorkflowID
                          };
            WorkflowManager.AddRun(run);
            return run;
        }
        #endregion

        #region notification
        private void LogErrorMessage(string msg)
        {
            this._EventLog.WriteEntry(msg, EventLogEntryType.Error);
            //string subj = "Workflow service error";
            //string body = msg;
            //string src = "Workflow Service";
            //this.SendNotification(body, subj, src);
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="msg"></param>
        private void LogInfo(string msg)
        {
            this._EventLog.WriteEntry(msg, EventLogEntryType.Information);
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="msg"></param>
        /// <param name="subj"></param>
        /// <param name="source"></param>
        private void SendNotification(string msg, string subj, string source)
        {
            try
            {
                string[] sendToEmailAddr =
                    ConfigurationManager.AppSettings.Get("SendToEmailAddress").Split(new char[] { ';', ',' });
                bool requireAuthentication = bool.Parse(ConfigurationManager.AppSettings.Get("RequireAuthentication"));
                bool enableSSL = bool.Parse(ConfigurationManager.AppSettings.Get("EnableSSL"));
                string smtpServerName = ConfigurationManager.AppSettings.Get("SmtpServer");
                MailMessage mail = new MailMessage();
                mail.From = new MailAddress("WorkflowService@" + Environment.UserDomainName + ".com");
                mail.Subject = "<b>" + subj + "</b>";
                foreach (string toAddr in sendToEmailAddr)
                {
                    mail.To.Add(new MailAddress(toAddr));
                }
                StringBuilder buffer = new StringBuilder();
                buffer.Append("<b>" + source + "</b>");
                buffer.Append("<hr/>");
                buffer.Append(msg);
                mail.Body = buffer.ToString();
                mail.IsBodyHtml = true;
                mail.Priority = MailPriority.High;
                SmtpClient mailClient = new SmtpClient(smtpServerName);
                if (requireAuthentication)
                {
                    string userName = ConfigurationManager.AppSettings.Get("EmailUserName");
                    string password = ConfigurationManager.AppSettings.Get("EmailPassword");
                    mailClient.Credentials = new NetworkCredential(userName, password);
                    mailClient.EnableSsl = enableSSL;
                }
                mailClient.Send(mail);
            }
            catch (Exception ex)
            {
                this.LogErrorMessage(ex.ToString());
                this.LogErrorMessage(ex.StackTrace);
            }
        }
        #endregion
    }
}
